package org.windowkit.toolkit.widget.tree;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Point;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.InputEvent;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import javax.swing.JComponent;
import javax.swing.JTree;
import javax.swing.TransferHandler;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

import org.windowkit.toolkit.widget.tree.TwoTieredTreeMoveEvent.TwoTieredTreeMoveListener;




/**
 * @author Rich O'Connell
 *
 */
public class TwoTieredTree<E> extends JComponent  {


    /**
	 *
	 */
	private static final long serialVersionUID = -3804038451659735143L;

	private JTree myTree;

    private DefaultMutableTreeNode rootNode;

    private DefaultTreeModel treeModel;

    private TwoTieredTreeCellRenderer cellRenderer;

    private TwoTieredTreeTransferHandler transferHandler;

    private Vector<TwoTieredTreeMoveListener<E>> moveListeners;


    /**
     *
     */
    public TwoTieredTree() {
        super();
        init();
    }

    public TwoTieredTree(final Hashtable<String, ArrayList<E>> data){
        super();
        init();
        addTreeData(data);
    }



    private void init(){

        moveListeners = new Vector<TwoTieredTreeMoveListener<E>>();

        rootNode = new DefaultMutableTreeNode();
        treeModel = new DefaultTreeModel(rootNode);
        myTree = new JTree(treeModel);
        myTree.setRootVisible(false);
        myTree.getSelectionModel().setSelectionMode(
                TreeSelectionModel.SINGLE_TREE_SELECTION);

        cellRenderer = new TwoTieredTreeCellRenderer();
        myTree.setCellRenderer(cellRenderer);



//        dragSource = new DragSourceEventHandler(this);
//        dropTarget = new DragDropEventHandler(this);

        transferHandler = new TwoTieredTreeTransferHandler(this);


        myTree.setTransferHandler(transferHandler);
        myTree.setDragEnabled(true);

        setLayout(new BorderLayout());
        add(myTree, BorderLayout.CENTER);
    }

    /**
     * Clears all of our folders and their leaves from our model.
     */
    public void clearAllData(){
        rootNode.removeAllChildren();
        treeModel.reload();
    }

    public void addTreeData(final Hashtable<String, ArrayList<E>> data){
        Enumeration<String> keys = data.keys();
        while(keys.hasMoreElements()){
            String folder = keys.nextElement();
            ArrayList<E> itemsToAdd = data.get(folder);
            addItemsImpl(folder, itemsToAdd);
        }
        treeModel.reload();
    }

    /**
     * Add item as a first tier item to the tree.
     * @param name
     */
    public void addFolder(final String name){
    	addFolderImpl(name);
        treeModel.reload();

    }

    /**
     * local use.
     * create a new node for a folder with the given name and add
     * it as a child of our root. Does nothing if a folder allready
     * exists with that name.
     * treeModel.reload() is not called.
     * The new node created is also returned.
     *
     * @param name
     *
     */
    private DefaultMutableTreeNode addFolderImpl(final String name){
        //check if it exists first
    	DefaultMutableTreeNode folder = getFolderNode(name);
        if(folder == null){
        	folder = new DefaultMutableTreeNode(name, true);
        	rootNode.add(folder);
        }
        return folder;
    }

    /**
     * Add an item to the second tier.
     * The item will be a child of the given folder.
     * If the folder doesnt exist it will be created first
     *
     * TODO right now there is no restriction on adding an
     * item that allready exists in that folder
     *
     * @param folder
     * @param itemToAdd
     */
    public void addItem(final String folder, final E itemToAdd){
        addItemImpl(folder, itemToAdd);
        treeModel.reload();
    }

    public void addItems(final String folder, final ArrayList<E> items){
    	addItemsImpl(folder, items);
    	treeModel.reload();
    }


    /**
     * Add the a new node to the node specified by the given folder name
     * Dupes are allowed
     * if the folder doesnt exist it will be created first.
     *
     * @param folder
     * @param itemToAdd
     */
    private void addItemImpl(final String folder, final E itemToAdd){

    	DefaultMutableTreeNode folderNode = getFolderNode(folder);
    	if(folderNode == null){
    		//create the folder fist
    		folderNode = addFolderImpl(folder);
    	}

    	folderNode.add(new DefaultMutableTreeNode(itemToAdd, false));

    }

    private void addItemsImpl(final String folder, final ArrayList<E> itemsToAdd){
    	DefaultMutableTreeNode folderNode = getFolderNode(folder);
    	if(folderNode == null){
    		//create the folder fist
    		folderNode = addFolderImpl(folder);
    	}

    	for(E item : itemsToAdd){
    		folderNode.add(new DefaultMutableTreeNode(item, false));
    	}
    }

    /**
     * Remove the item from the folder.
     *
     * @param folder
     * @param item
     */
    public void removeItem(final String folder, final E item){
    	DefaultMutableTreeNode node = getItemNode(folder, item);
    	if(node != null){
    		node.removeFromParent();
    		treeModel.reload();
    	}
    }


    public boolean isSelected(){
    	return !myTree.getSelectionModel().isSelectionEmpty();
    }

    /**
     * Is a leaf selected on the tree.
     *
     * @return - True only if the node selected is a leaf.
     * False if no node is selected at all.
     */
    public boolean isLeafSelected(){
        DefaultMutableTreeNode selection = getSelectedNode();
        if(selection != null){
        	return isNodeALeaf(selection);
        }
        return false;
    }

    /**
     * local use
     * is the given tree node a leaf
     * i.e. if it's parent's parent is the rootNode
     *
     * @param node
     * @return
     */
    protected boolean isNodeALeaf(final TreeNode node){
        TreeNode parent = node.getParent();
        if(parent !=null){
            return !parent.equals(rootNode);
        }
        return false;
    }

    /**
     * local use
     * is the given node a folder
     * i.e. is its parent the rootNode
     *
     * @param node
     * @return
     */
    protected boolean isNodeAFolder(final TreeNode node){
        TreeNode parent = node.getParent();
        if(parent != null){
            return parent.equals(rootNode);
        }
        return false;
    }

    /**
     * Get the currently selected item.
     * If no item is selected returns null.
     * If just a folder is selected and no actual item this will return null.
     *
     * @return
     */
    public E getSelectedItem(){
    	E item = null;
    	DefaultMutableTreeNode node = getSelectedNode();
        if(node != null){
            if(isNodeALeaf(node)){
            	item = (E) node.getUserObject();

            }
        }
        return item;
    }



    /**
     * Get the selected folder.
     * If a leaf item is selected then the folder that that leaf
     * lives in is returned.
     *
     * @return
     */
    public String getSelectedFolder(){
        String folder = null;
        DefaultMutableTreeNode node = getSelectedNode();

        if(node != null){
            if(isNodeAFolder(node)){
                folder = node.getUserObject().toString();
            }
            if(isNodeALeaf(node)){
                folder = ((DefaultMutableTreeNode)node.getParent()).getUserObject().toString();
            }
        }
        return folder;
    }

    /**
     * For local use.
     * Returns the current selected node.
     * null if no node is selected.
     * @return
     */
    protected DefaultMutableTreeNode getSelectedNode(){
        DefaultMutableTreeNode node = null;
        TreePath path = myTree.getSelectionPath();
        if(path != null){
        	node =  (DefaultMutableTreeNode)path.getLastPathComponent();
        }
        return node;
    }


    /**
     * local use
     * get the folder node for the folder with the given name
     * null if that folder doesnt exist
     *
     * @param folder
     * @return
     */
    protected DefaultMutableTreeNode getFolderNode(final String folder){
    	DefaultMutableTreeNode ret = null;
    	DefaultMutableTreeNode currentFolder;
    	for(int i = 0; i < rootNode.getChildCount(); i++){
    		currentFolder = (DefaultMutableTreeNode)rootNode.getChildAt(i);
    		if(currentFolder.getUserObject().toString().equals(folder)){
    			ret = currentFolder;
    			break;
    		}
    	}

    	return ret;
    }

    /**
     * For local use.
     * Get the item node that the given item is stored at.
     * null if the folder/item combo can't be found.
     *
     * @param folder
     * @param item
     * @return
     */
    protected DefaultMutableTreeNode getItemNode(final String folder, final E item){
    	DefaultMutableTreeNode ret = null;
    	DefaultMutableTreeNode folderNode = getFolderNode(folder);
    	DefaultMutableTreeNode currentNode;

    	for(int i = 0; i < folderNode.getChildCount(); i++){
    		currentNode = (DefaultMutableTreeNode) folderNode.getChildAt(i);
    		if(currentNode.getUserObject().equals(item)){
    			ret = currentNode;
    			break;
    		}
    	}

    	return ret;
    }

    /**
     * Set the given folder to be selected.
     * @param folderName
     */
    public void setSelectedFolder(final String folderName){
    	DefaultMutableTreeNode folderNode = getFolderNode(folderName);
    	setSelectedNode(folderNode);
    }


    public void setSelectedItem(final String folder, final E item){
        DefaultMutableTreeNode itemNode = getItemNode(folder, item);
        setSelectedNode(itemNode);
    }

    /**
     * local use.
     * sets the selection path and scrolls to visible
     * @param path
     */
    protected void setSelectedNode(final DefaultMutableTreeNode node){
        if(node != null){
        	TreePath path = new TreePath(node.getPath());
        	myTree.setSelectionPath(path);
        	myTree.scrollPathToVisible(path);
        }
    }


    /* (non-Javadoc)
     * @see javax.swing.JTree#addTreeSelectionListener(javax.swing.event.TreeSelectionListener)
     */
    public void addTreeSelectionListener(final TreeSelectionListener tsl) {
        myTree.addTreeSelectionListener(tsl);
    }

    /*
     * Item Move Event support
     */

    public void addItemMoveListener(final TwoTieredTreeMoveListener<E> l){
        moveListeners.add(l);
    }
    public void clearItemMoveListeners(){
        moveListeners.clear();
    }

    protected void fireItemMovedEvent(final E item, final String oldFolder, final String newFolder){
    	for(TwoTieredTreeMoveListener<E> l : moveListeners){
    		l.itemHasMoved(new TwoTieredTreeMoveEvent<E>(this, item, oldFolder, newFolder));
    	}

    }

    /**
     * Restructures the tree and fires an event to our listeners letting
     * them know about the change.
     *
     * @param itemToMove - The node being moved, this must be a leaf node
     * @param newLocation - Then new parent, this must be a folder node
     */
    protected void moveItem(final DefaultMutableTreeNode itemToMove, final DefaultMutableTreeNode newLocation){
    	if(!itemToMove.getParent().equals(newLocation)){
    		String oldHome = ((DefaultMutableTreeNode)itemToMove.getParent()).getUserObject().toString();
    		String newHome = newLocation.getUserObject().toString();

    		itemToMove.removeFromParent();
    		newLocation.add(itemToMove);
    		fireItemMovedEvent((E)itemToMove.getUserObject(), oldHome, newHome);
    		treeModel.reload();
    	}
	}



    /* (non-Javadoc)
     * @see javax.swing.JTree#getClosestPathForLocation(int, int)
     */
    public TreePath getClosestPathForLocation(final int x, final int y) {
        return myTree.getClosestPathForLocation(x, y);
    }



    //TODO this isn't used
    protected boolean isPointOverLeaf(final Point p){
        TreePath path = myTree.getPathForLocation(p.x , p.y);
        if(path != null){
            Object node = path.getLastPathComponent();
            if(node instanceof TreeNode){
                return isNodeALeaf((TreeNode)node);
            }
        }
        return false;
    }

    /**
     * Specialize our cell renderer so that first tier items with no children
     * are not drawn as leafs.
     *
     */
    protected class TwoTieredTreeCellRenderer extends DefaultTreeCellRenderer{

        /**
		 *
		 */
		private static final long serialVersionUID = -7918393847682348455L;

		@Override
        public Component getTreeCellRendererComponent(final JTree tree, final Object value,
                final boolean sel, final boolean expanded, boolean leaf, final int row, final boolean hasFucus) {
            if(value instanceof TreeNode){
                leaf = isNodeALeaf((TreeNode)value);
            }
            return super.getTreeCellRendererComponent(tree,value,sel,expanded,leaf,row,hasFucus);
        }

    }



    /*
     *
     * Drag n Drop support
     *
     */

    private class TwoTieredTreeTransferHandler extends TransferHandler {
        /**
		 *
		 */
		private static final long serialVersionUID = -588382357444617862L;

		private final TwoTieredTree<E> ttTree;

        private DefaultMutableTreeNode nodeBeingDragged;

        public TwoTieredTreeTransferHandler(final TwoTieredTree<E> ttTree){
            super();
            this.ttTree = ttTree;
        }

        @Override
        public void exportAsDrag(final JComponent comp, final InputEvent e, final int action) {
            super.exportAsDrag(comp, e, action);
        }

        @Override
        protected Transferable createTransferable(final JComponent c) {
            Transferable trans = new TransferableTreeNode(ttTree.getSelectedNode());
            nodeBeingDragged = ttTree.getSelectedNode();
            return trans;
        }

        @Override
        @SuppressWarnings("unused")
        public boolean canImport( final JComponent comp, final DataFlavor[] transferFlavors) {
            return true;
        }

        @Override
        @SuppressWarnings("unused")
        public boolean importData( final JComponent comp, final Transferable t) {

            Point pos = ttTree.getMousePosition();
            TreePath path = ttTree.getClosestPathForLocation(pos.x, pos.y);
            if(path != null){
                TreeNode node = (TreeNode)path.getLastPathComponent();
                if(ttTree.isNodeAFolder(node)){

                    ttTree.moveItem(nodeBeingDragged, (DefaultMutableTreeNode)node);
                    return true;
                }
                return false;

            }
            return false;
        }

        @Override
        @SuppressWarnings("unused")
        protected void exportDone(final JComponent source, final Transferable data, final int action) {}

        @Override
        public int getSourceActions(@SuppressWarnings("unused") final JComponent c) {
           return MOVE;
        }
    }

    private class TransferableTreeNode implements Transferable{

        DataFlavor DMT_FLAVOR = new DataFlavor(DefaultMutableTreeNode.class, "DefaultMutableTreeNode");
        private final DataFlavor[] flavors = {DMT_FLAVOR};
        private final DefaultMutableTreeNode node;

        public TransferableTreeNode(final DefaultMutableTreeNode node){
            this.node = node;
        }

        public DataFlavor[] getTransferDataFlavors() {
            return flavors;
        }

        public boolean isDataFlavorSupported(final DataFlavor flavor) {
            return flavor.equals(DMT_FLAVOR);
        }

        public Object getTransferData(final DataFlavor flavor) throws UnsupportedFlavorException, IOException {
            if (isDataFlavorSupported(flavor)) {
                return node;
            }
            throw new UnsupportedFlavorException(flavor);
        }
    }





}
